﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Dycox.ComponentModel;

namespace Dycox.Validation
{
    public static class ValidationManager 
    {

        //public ValidationManager()
        //{

        //}

        //public ValidationManager(IContainer container)
        //    : this()
        //{
        //    if (container != null)
        //        container.Add(this);
        //}

        public static MessageSet Validate(object obj, bool throwError = true)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");

            ManagerValidationContext context = new ManagerValidationContext(obj);
            
            Validate(context);

            MessageSet msgSet = context.Messages;

            if (msgSet.Level == MessageKind.Error && throwError)
            {
                throw new ValidationException(msgSet.ToString(MessageKind.Error));
            }

            return msgSet;

        }

        private static void Validate(ManagerValidationContext context)
        {
            object obj = context.Instance;
            ICustomValidate valCustom = obj as ICustomValidate;

            if (valCustom == null || valCustom.PerformDefaultValidation)
            {
                foreach (PropertyDescriptor desp in context.Properties)
                {
                    PropertyValidationContext ctx = new PropertyValidationContext(obj, desp);
                    if (valCustom == null || valCustom.ShouldValidate(ctx))
                    {
                        if (context.BeginValidate(ctx))
                        {
                            ValidatorAttribute[] array = desp.Attributes.OfType<ValidatorAttribute>().ToArray();

                            foreach (ValidatorAttribute va in array)
                            {
                                
                                if (ShouldValidate(va, ctx))
                                {
                                    Validator validator = va.CreateValidator(ctx);
                                    if (validator.IsValid(ctx.PropertyValue, ctx) == ValidationResult.Invalid)
                                    {
                                        MessageKind kind;
                                        kind = va.IsWarning ? MessageKind.Warning : MessageKind.Error;
                                        context.AddMessage(validator.GetErrorMessage(ctx), kind, desp);
                                    }
                                }
                            }

                            if (ctx.IsCollection && ctx.Behavior != ValidationBehavior.IgnoreItems)
                            {
                                IEnumerator enu;

                                try { enu = ctx.GetEnumerator(); }
                                catch (ObjectDisposedException) { enu = null; }

                                if (enu != null)
                                {
                                    try
                                    {
                                        while (enu.MoveNext())
                                        {
                                            context.Enter(enu.Current, desp);
                                            Validate(context);
                                            context.Exit();
                                        }
                                    }
                                    finally
                                    {
                                        if (enu is IDisposable)
                                            ((IDisposable)enu).Dispose();
                                    }
                                }
                            }
                        }
                    }
                }
            }

            if (valCustom != null)
            {
                valCustom.Validate(context);
            }
        }

        static Dictionary<Type, Dictionary<string, Delegate>> _delegates;

        private static bool ShouldValidate(ValidatorAttribute va, PropertyValidationContext context)
        {
            string condition = va.Condition;
            if (!string.IsNullOrWhiteSpace(condition))
            {
                if (_delegates == null)
                    _delegates = new Dictionary<Type, Dictionary<string, Delegate>>();

                Dictionary<string, Delegate> dict;
                Delegate del;

                if (!_delegates.TryGetValue(context.Instance.GetType(), out dict))
                {
                    dict = new Dictionary<string, Delegate>();
                    _delegates.Add(context.Instance.GetType(), dict);
                }

                if (!dict.TryGetValue(condition, out del))
                {
                    del = System.Linq.Dynamic.DynamicExpression.ParseLambda(context.Instance.GetType(), typeof(bool), condition).Compile();
                    dict.Add(condition, del);
                }

                return (bool)del.DynamicInvoke(context.Instance);
            }

            return true;
        }

        internal static bool IsValidatorTypeValid(Type type)
        {
            if (type == null || type.IsAbstract || !type.IsSerializable)
                return false;

            if (!type.IsSubclassOf(typeof(Validator)))
                return false;

            if (type.GetConstructor(Type.EmptyTypes) == null)
                return false;

            return true;
        }

        internal static Type[] GetValidValidatorTypes(ITypeDescriptorContext context)
        {
            ITypeDiscoveryService tdsvc = null;

            if (context != null)
                tdsvc = (ITypeDiscoveryService)context.GetService(typeof(ITypeDiscoveryService));

            System.Collections.ICollection types;
            if (tdsvc != null)
            {
                types = tdsvc.GetTypes(typeof(Validator), true);
            }
            else
                types = typeof(Validator).Assembly.GetExportedTypes();


            return types.Cast<Type>().Where(o => IsValidatorTypeValid(o)).OrderBy(o => o.Name).ToArray();

        }

    }

    public class ManagerValidationContext
    {
        internal ManagerValidationContext(object instance)
        {
            _validatingObjects = new List<object>();
            _messages = new MessageSet();
            _members = new Stack<InstanceContext>(3);

            _members.Push(new InstanceContext(instance, null));
        }

        private List<object> _validatingObjects;

        internal bool BeginValidate(PropertyValidationContext context)
        {
            if (context.Behavior == ValidationBehavior.Ignore || _validatingObjects.Contains(context.Instance))
                return false;

            return true;
        }

        private MessageSet _messages;

        internal MessageSet Messages
        {
            get { return _messages; }
        }

        private Stack<InstanceContext> _members;

        internal void Enter(object instance, PropertyDescriptor member)
        {
            if (instance == null || member == null)
                throw new ArgumentNullException();

            _members.Push(new InstanceContext(instance, member));
        }

        internal PropertyDescriptor Exit()
        {
            if (_members.Count == 0)
                throw new InvalidOperationException();
            return _members.Pop().PropertyDescriptor;
        }

        private string GetPath(PropertyDescriptor member)
        {
            string name = member.Name;

            if (_members.Count > 1)
            {
                name = string.Join(Type.Delimiter.ToString(), _members.Reverse().Skip(1).Select(o => o.PropertyDescriptor.Name)) + Type.Delimiter + name;
            }

            return name;
        }

        internal PropertyDescriptorCollection Properties
        {
            get
            {
                return _members.Peek().Properties;
            }
        }

        internal object Instance
        {
            get
            {
                return _members.Peek().Instance;
            }
        }

        internal ValidationMessageEntry AddMessage(string text, MessageKind kind, PropertyDescriptor property)
        {
            ValidationMessageEntry entry = new ValidationMessageEntry(text, kind, property, GetPath(property));
            _messages.Add(entry);
            return entry;
        }

        public ValidationMessageEntry AddMessage(string text, MessageKind kind, string propertyName)
        {
            if (propertyName == null)
                throw new ArgumentNullException("propertyName");

            PropertyDescriptor pd = this.Properties[propertyName];
            if (pd == null)
                throw new ArgumentException(SR.GetString("invalid_property_name", propertyName));

            return AddMessage(text, kind, pd);
        }

        public void AddMessages(MessageSet msgSet, string propertyName)
        {
            foreach (var ent in msgSet)
            {
                if (ent.Kind != MessageKind.Information)
                    AddMessage(ent.Text, ent.Kind, propertyName);
            }
        }
    }

    internal class InstanceContext
    {
        public InstanceContext(object instance, PropertyDescriptor property)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            _instance = instance;
            _propertyDescriptor = property;
            
        }

        private object _instance;

        public object Instance
        {
            get { return _instance; }
        }

        PropertyDescriptorCollection _properties;

        public PropertyDescriptorCollection Properties
        {
            get
            {
                if (_properties == null)
                    _properties = TypeDescriptor.GetProperties(_instance);
                return _properties;
            }
        }

        private PropertyDescriptor _propertyDescriptor;

        public PropertyDescriptor PropertyDescriptor
        {
            get { return _propertyDescriptor; }
        }

    }

}
